[FEATURE] Add constructed generic candidate support and popup performance improvements#100
Open
dichotomy-corp wants to merge 2 commits intomackysoft:mainfrom
Conversation
Add support for discovering and constructing closed generic candidate types (e.g. ConstantValueProvider<int>) when the field type is a closed generic interface or base class (Unity 2023.2+). Previously, only non-generic concrete classes that explicitly closed the type parameter were shown in the popup. Changes: - Relax intrinsic type policy to allow constructed generics through - Infer type arguments and construct closed types via MakeGenericType - Add generic-aware display names for popup menu and inline labels - Merge dual assembly scans into a single pass - Remove redundant filtering in TypeCandiateService - Cache attribute lookups, type paths, and nicified names - Pre-sort type array once at construction instead of on every Show
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
This PR adds support for constructed generic candidate types in the SubclassSelector popup when the field type is a closed generic interface or base class (Unity 2023.2+), along with several performance improvements to the popup display path.
Motivation
Currently, when a field is declared as
[SerializeReference, SubclassSelector] IValueProvider<int>, the popup correctly discovers non-generic concrete classes that explicitly close the type parameter (e.g.class ConcreteIntProvider : IValueProvider<int>). However, it does not discover generic classes likeConstantValueProvider<T> : IValueProvider<T>— even though the type arguments can be inferred from the field type and the constructed typeConstantValueProvider<int>is fully serializable.This means users are forced to write one boilerplate subclass per type argument:
This PR removes that requirement by constructing closed generic types at editor time via
MakeGenericType, and also addresses a few performance bottlenecks that become more noticeable when scanning all loaded assemblies.Example
A note on design decisions
I understand that the existing
!candiateType.IsGenericTypefilter inDefaultIntrinsicTypePolicyand the overall architecture were likely deliberate choices. If any of these changes conflict with design goals I'm not aware of — such as keeping the intrinsic policy conservative, avoidingMakeGenericTypein editor code, or maintaining the separation of filtering responsibilities between provider and service — I'm completely open to adjusting or withdrawing parts of this PR. I've tried to keep the changes minimal and contained to the 2023.2+ code path where possible.Changes made
1. Constructed generic candidate support (Unity 2023.2+)
DefaultIntrinsicTypePolicy.cs!candiateType.IsGenericTypeto!candiateType.ContainsGenericParameters. This allows closed constructed generics (e.g.ConstantValueProvider<int>) to pass through while still blocking open generic definitions (e.g.ConstantValueProvider<T>). This is the only change that affects the non-2023.2 code path.Unity_2023_2_OrNewer_TypeCandiateProvider.csGetTypesWithGenericto detect open generic type definitions in the assembly scan, attempt to infer and map their type parameters from the field's base type, and construct closed generic types viaMakeGenericType.TryCloseGenericType(walks interfaces and base class chain to find matching generic definitions),TryMapTypeArguments(maps generic parameters from the candidate to concrete type arguments from the field type), andTryMakeGenericTypeSafe(wrapsMakeGenericTypein a try-catch for constraint violations).2. Generic-aware display names
TypeMenuUtility.csGetNiceGenericNameandGetNiceGenericFullNamehelpers that produce human-readable names for constructed generic types (e.g.ConstantValueProvider<Int32>instead ofConstantValueProvider`1[[System.Int32, ...]]).GetSplittedTypePathto use these helpers for the popup menu paths.OrderByTypeto useGetNiceGenericNameas the fallback sort key.SubclassSelectorDrawer.csGetTypeNameto useGetNiceGenericNameso that the inline label displays correctly for constructed generic types (the backtick in rawtype.NamecausedObjectNames.NicifyVariableNameto produce an empty string).3. Performance improvements
These changes address noticeable lag (~500ms) when opening the popup, particularly when generic field types trigger a full assembly scan.
TypeCandiateService.csintrinsicTypePolicy.IsAllowedandtypeCompatibilityPolicy.IsCompatiblefiltering inGetDisplayableTypes, since both providers already apply these filters internally. Simplified the constructor to only requireITypeCandiateProvider.TypeSearchService.csTypeCandiateServiceconstruction to match the simplified constructor.TypeMenuUtility.csDictionary<Type, AddTypeMenuAttribute>cache forGetAttributeto avoid repeatedAttribute.GetCustomAttributereflection calls (invoked multiple times per type during sorting and path resolution on every popup open).Dictionary<Type, string[]>cache forGetSplittedTypePathresults.Dictionary<string, string>cache forObjectNames.NicifyVariableNameresults via a newCachedNicifyVariableNamemethod. All caches include null guards for safety.AdvancedTypePopup.csOrderByTypesorting fromBuildRoot(called by Unity on everyShow) intoSetTypes(called once at construction), so the type array is sorted once and reused across popup opens.All static caches are naturally cleared on domain reload (script recompilation), so newly added subclasses are always picked up.